// Electron 主进程入口：窗口管理、系统托盘、悬浮球逻辑（无吸附）
// 适配 Vue2 渲染进程，包含主界面与悬浮球两个窗口

const { app, BrowserWindow, Tray, nativeImage, ipcMain, screen, Menu, Notification, dialog, desktopCapturer, clipboard } = require('electron')
// const { registerOpenCvHandlers } = require('./OpenCvService')
const path = require('path')
const fs = require('fs')
// 引入 robotjs（可选）
let robot = null
try { robot = require('robotjs') } catch (_) { robot = null }
// 引入 @nut-tree/nut-js（可选，作为 robotjs 回退）
let nut = null
try {
  nut = require('@nut-tree/nut-js')
  // 降低自动延迟，加快动作
  nut.keyboard.config.autoDelayMs = 0
  nut.mouse.config.autoDelayMs = 0
} catch (_) { nut = null }
// 搜索引擎模块：索引、搜索、打开
const { AppIndexer } = require('./AppIndexer')
const { FileIndexer } = require('./FileIndexer')
const { SearchService } = require('./SearchService')
const { OpenService } = require('./OpenService')
const { RegistryIndexer } = require('./RegistryIndexer')
const { FileSearchEngine, defaultDirs } = require('./FileSearchEngine')

let appIndexer = null
let fileIndexer = null
let searchService = null
let openService = null
let registryIndexer = null
let fileSearchEngine = null

let mainWindow = null
let floatWindow = null
let tray = null
let forceQuit = false

// 悬浮球拖拽状态
const dragState = {
  dragging: false,
  offset: { x: 0, y: 0 },
}

const isDev = !app.isPackaged
const DEV_SERVER_URL = process.env.VUE_DEV_SERVER_URL || 'http://localhost:8080/'

function resolveTrayIcon() {
  // 托盘图标：要求使用 public/favicon.ico
  const devIcon = path.resolve(__dirname, '../../public/favicon.ico')
  if (fs.existsSync(devIcon)) return devIcon
  // 打包后：extraResources 已复制到 resourcesPath 下
  return path.join(process.resourcesPath, 'favicon.ico')
}

const { pathToFileURL } = require('url')
function getRendererUrl(hash = '/') {
  // 渲染进程地址：开发环境使用 dev server，生产环境使用打包后的 dist
  const normalizedHash = hash.startsWith('/') ? hash : `/${hash}`
  if (isDev) return `${DEV_SERVER_URL}#${normalizedHash}`
  // 正确构造 file:// URL（Windows 需要使用 pathToFileURL 处理反斜杠与斜杠数量）
  const fileUrl = pathToFileURL(path.join(__dirname, '../../dist/index.html')).toString()
  return `${fileUrl}#${normalizedHash}`
}

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 960,
    height: 600,
    show: false, // 启动后隐藏，托盘点击时显示
    frame: true,
    skipTaskbar: true, // 不在任务栏显示
    autoHideMenuBar: true,
    title: '声声不息',
    icon: path.join(__dirname, '../../public/favicon.ico'),
    webPreferences: {
      contextIsolation: true,
      preload: path.join(__dirname, '../preload/index.js'),
    },
  })

  // mainWindow.openDevTools()
  mainWindow.loadURL(getRendererUrl('/'))

  mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
    console.error('Main window load failed:', { errorCode, errorDescription, validatedURL })
  })
  mainWindow.webContents.on('did-finish-load', () => {
    // 可选：如需启动即显示，可在此调用 mainWindow.show()
    // mainWindow.show()
    if (isDev) {
      try { mainWindow.webContents.openDevTools({ mode: 'detach' }) } catch (_) {}
    }
    // 生产环境默认显示主窗口，避免用户以为未启动
    if (!isDev) {
      try { mainWindow.show() } catch (_) {}
    }
  })

  // 关闭主界面时隐藏到托盘（允许显式退出）
  mainWindow.on('close', (e) => {
    if (!forceQuit) {
      e.preventDefault()
      mainWindow.hide()
    }
  })
}

function createFloatWindow() {
  floatWindow = new BrowserWindow({
    width: 100,
    height: 100,
    x: 100,
    y: 100,
    frame: false,
    transparent: true,
    resizable: false,
    skipTaskbar: true,
    alwaysOnTop: true, // 置顶显示
    webPreferences: {
       webSecurity: false,
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, '../preload/index.js'),
    },
  })
  floatWindow.loadURL(getRendererUrl('/floatball'))
  floatWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
    console.error('Float window load failed:', { errorCode, errorDescription, validatedURL })
  })
  floatWindow.webContents.on('did-finish-load', () => {
    // 在开发环境打开 DevTools（独立窗口），再确保置顶
    if (isDev) {
      try { floatWindow.webContents.openDevTools({ mode: 'detach' }) } catch (_) {}
    }
    // 确保置顶显示
    floatWindow.setAlwaysOnTop(true, 'screen-saver')
  })
  floatWindow.setVisibleOnAllWorkspaces(true)
}

function createTray() {
  const iconPath = resolveTrayIcon()
  const image = nativeImage.createFromPath(iconPath)

  tray = new Tray(image)
  tray.setToolTip('声声不息')

  // 托盘右键菜单：退出应用程序
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '退出应用程序',
      click: () => {
        forceQuit = true
        app.quit()
      }
    }
  ])
  tray.setContextMenu(contextMenu)

  // 单击托盘图标：显示/隐藏主界面
  tray.on('click', () => {
    if (!mainWindow) return
    if (mainWindow.isVisible()) {
      mainWindow.hide()
    } else {
      mainWindow.show()
      mainWindow.focus()
    }
  })
}

function broadcastStatus(status, extra = {}) {
  try {
    const payload = { status, ...extra }
    if (mainWindow && !mainWindow.isDestroyed()) {
      mainWindow.webContents.send('search:status', payload)
    }
    if (floatWindow && !floatWindow.isDestroyed()) {
      floatWindow.webContents.send('search:status', payload)
    }
  } catch (err) {
    console.error('状态广播失败:', err)
  }
}

// 设置管理：工作目录数组持久化与验证
const SETTINGS_FILE = path.join(process.cwd(), 'workdirs.json')
function isDirectorySafe(p) {
  try {
    if (!p || typeof p !== 'string') return false
    const stat = fs.statSync(p)
    return stat && stat.isDirectory()
  } catch (_) { return false }
}
function getDesktopDir() {
  try { return app.getPath('desktop') } catch (_) { return path.join(require('os').homedir(), 'Desktop') }
}
function readWorkDirs() {
  try {
    if (!fs.existsSync(SETTINGS_FILE)) return null
    const raw = fs.readFileSync(SETTINGS_FILE, 'utf8')
    const arr = JSON.parse(raw)
    if (!Array.isArray(arr)) return null
    const dirs = arr.filter(isDirectorySafe)
    return dirs.length ? dirs : null
  } catch (_) { return null }
}
function writeWorkDirs(dirs) {
  try {
    const valid = (dirs || []).filter(isDirectorySafe)
    fs.writeFileSync(SETTINGS_FILE, JSON.stringify(valid, null, 2), 'utf8')
    return true
  } catch (err) {
    console.error('写入工作目录失败:', err)
    return false
  }
}
// 新增：获取当前工作根目录（取 settings 中第一个目录；无则桌面）
function getActiveWorkDir() {
  const dirs = readWorkDirs()
  const desktop = getDesktopDir()
  const root = (Array.isArray(dirs) && dirs.length ? dirs[0] : desktop)
  return isDirectorySafe(root) ? root : desktop
}
// 新增：相对路径安全校验（禁止绝对路径、盘符与上溯）
function isRelPathSafe(rel) {
  try {
    if (!rel || typeof rel !== 'string') return false
    // 禁止非法字符与盘符冒号
    if (/[<>:"|?*]/.test(rel)) return false
    // 防止上溯与绝对路径
    if (rel.includes('..')) return false
    if (path.isAbsolute(rel)) return false
    return true
  } catch (_) { return false }
}
// 新增：解析到工作目录内的绝对路径（并确保仍在根目录下）
function resolveWithinWorkRoot(rel) {
  try {
    if (!isRelPathSafe(rel)) return null
    const root = getActiveWorkDir()
    const full = path.resolve(root, rel)
    const diff = path.relative(root, full)
    if (!diff || (!diff.startsWith('..') && !path.isAbsolute(diff))) return full
    return null
  } catch (_) { return null }
}
function registerIpc() {
  // 右键拖拽：开始
  ipcMain.on('floatball:drag-start', (event, offset) => {
    dragState.dragging = true
    dragState.offset = offset || { x: 0, y: 0 }
  })

  // 右键拖拽：移动（渲染进程每次在鼠标移动时提供全局指针坐标）
  ipcMain.on('floatball:drag-move', (event, globalPoint) => {
    if (!dragState.dragging || !floatWindow) return
    const x = Math.round(globalPoint.x - dragState.offset.x)
    const y = Math.round(globalPoint.y - dragState.offset.y)
    floatWindow.setPosition(x, y)
  })

  // 右键拖拽：结束
  ipcMain.on('floatball:drag-end', () => {
    dragState.dragging = false
  })

  // 提供光标全局坐标（供渲染进程获取）
  ipcMain.handle('system:get-cursor', () => {
    return screen.getCursorScreenPoint()
  })

  // 系统通知：通过主进程触发原生系统通知
  ipcMain.on('system:notify', (event, options = {}) => {
    try {
      const { title, body, silent } = options
      const notifyIcon = resolveTrayIcon()
      if (Notification && Notification.isSupported()) {
        const notice = new Notification({ title, body, silent: !!silent, icon: notifyIcon })
        notice.show()
      } else if (mainWindow) {
        // 兜底：不支持系统通知时用窗口标题闪烁或日志
        mainWindow.webContents.send('log:notify-fallback', { title, body })
      }
    } catch (err) {
      console.error('系统通知失败:', err)
    }
  })

  // 搜索：返回排序后的结果集
  // ipcMain.handle('search:target', async (event, keyword) => {
  //   const key = (typeof keyword === 'string' ? keyword.trim() : '')
  //   if (!key) {
  //     return { success: false, message: '关键字不能为空', results: [] }
  //   }
  //   broadcastStatus('searching', { keyword: key })
  //   try {
  //     const results = searchService ? searchService.search(key) : []
  //     if (!results.length) {
  //       broadcastStatus('search_empty', { keyword: key })
  //       return { success: false, message: '未找到匹配的应用或文件', results: [] }
  //     }
  //     broadcastStatus('search_done', { count: results.length })
  //     return { success: true, results }
  //   } catch (err) {
  //     console.error('搜索失败:', err)
  //     broadcastStatus('search_error', { error: err.message })
  //     return { success: false, message: '搜索异常，请稍后重试', results: [] }
  //   }
  // })

  // 打开：基于 path 与 type 执行打开
  ipcMain.handle('open:target', async (event, target) => {
    const { path: p, type, name } = target || {}
    const label = (typeof name === 'string' && name) ? name : (p ? path.basename(p) : '')
    broadcastStatus('opening', { path: p, type, message: label ? `正在打开目标${label}` : '正在打开目标' })
    const res = await openService.open(p, type)
    const msg = res.success
      ? (label ? `已打开目标${label}` : '已打开目标')
      : (label ? `打开失败：${label}，原因：${res.message}` : `打开失败，原因：${res.message}`)
    broadcastStatus(res.success ? 'open_success' : 'open_failed', { message: msg, path: p, type })
    return res
  })

  // 一步执行：搜索并打开最优结果（严格匹配：完全匹配 -> 递减前缀 -> 失败）
  ipcMain.handle('search_open:keyword', async (event, keyword) => {
    const keyRaw = (typeof keyword === 'string' ? keyword.trim() : '')
    const key = keyRaw.toLowerCase()
    if (!key) {
      return { success: false, message: '关键字不能为空' }
    }
    broadcastStatus('searching', { keyword: key })

    // 获取应用与文件的候选集合
    const appCandidates = searchService ? (searchService.searchApps(key) || []) : []
    const fileCandidates = searchService ? (searchService.searchFiles(key) || []) : []
    const allCandidates = [...appCandidates, ...fileCandidates]

    // 帮助函数：判断是否匹配（精确或前缀）
    const getName = (item) => {
      if (!item) return ''
      if (item.name) return String(item.name).toLowerCase()
      if (item.path) {
        try { return path.basename(item.path).toLowerCase() } catch (_) { return '' }
      }
      return ''
    }

    // 1) 完全匹配优先（跨类型）
    let best = allCandidates.find(it => getName(it) === key)

    // 2) 若无完全匹配，进行递减前缀匹配（跨类型）
    if (!best) {
      for (let len = key.length - 1; len >= 1; len--) {
        const prefix = key.slice(0, len)
        best = allCandidates.find(it => getName(it).startsWith(prefix))
        if (best) break
      }
    }

    // 3) 若仍无匹配，则失败（不打开任何目标）
    if (!best) {
      broadcastStatus('search_empty', { keyword: key })
      broadcastStatus('open_failed', { message: `打开失败：未找到匹配的目标“${keyRaw}”` })
      return { success: false, message: '未找到匹配的应用或文件' }
    }

    const label = getName(best)
    broadcastStatus('opening', { path: best.path, type: best.type, message: label ? `正在打开目标${label}` : '正在打开目标' })
    const res = await openService.open(best.path, best.type)
    const msg = res.success
      ? (label ? `已打开目标${label}` : '已打开目标')
      : (label ? `打开失败：${label}，原因：${res.message}` : `打开失败，原因：${res.message}`)
    broadcastStatus(res.success ? 'open_success' : 'open_failed', { message: msg, path: best.path, type: best.type })
    return res
  })

  // 专门：按文件名搜索并打开（支持 directories+keyword+options）
  ipcMain.handle('search_open:file', async (event, payload, keywordMaybe, optionsMaybe) => {
    // 兼容旧签名：传入字符串即仅 keyword
    let directories, keyword, options
    if (Array.isArray(payload)) {
      directories = payload
      keyword = typeof keywordMaybe === 'string' ? keywordMaybe : ''
      options = typeof optionsMaybe === 'object' ? optionsMaybe : {}
    } else if (typeof payload === 'object' && payload) {
      directories = Array.isArray(payload.directories) ? payload.directories : []
      keyword = typeof payload.keyword === 'string' ? payload.keyword : ''
      options = typeof payload.options === 'object' ? payload.options : {}
    } else {
      directories = defaultDirs()
      keyword = typeof payload === 'string' ? payload : ''
      options = {}
    }

    const key = (typeof keyword === 'string' ? keyword.trim().toLowerCase() : '')
    if (!key) return { success: false, error: '关键字不能为空', code: 'EMPTY_KEYWORD' }

    const dirs = (directories || []).filter(d => typeof d === 'string' && !!d)
    const effDirs = dirs.length ? dirs : defaultDirs()

    broadcastStatus('searching', { keyword: key, type: 'file', dirs: effDirs })
    try {
      // 取消上一次搜索（实现连续调用的取消机制）
      fileSearchEngine && fileSearchEngine.cancelAll()
      const { success, matches, best } = await fileSearchEngine.searchInDirs(effDirs, key, options || {})
      if (!success || !best) {
        broadcastStatus('search_empty', { keyword: key, type: 'file' })
        return { success: false, error: '未找到匹配的文件', code: 'NOT_FOUND', matches }
      }
      const label = (best && best.name) ? best.name : (best && best.path ? path.basename(best.path) : '')
      broadcastStatus('opening', { path: best.path, type: 'file', message: label ? `正在打开目标${label}` : '正在打开目标' })
      const res = await openService.open(best.path, 'file')
      const msg = res.success
        ? (label ? `已打开目标${label}` : '已打开目标')
        : (label ? `打开失败：${label}，原因：${res.message}` : `打开失败，原因：${res.message}`)
      broadcastStatus(res.success ? 'open_success' : 'open_failed', { message: msg, path: best.path, type: 'file' })
      return res.success ? { success: true, filePath: best.path, matches } : { success: false, error: res.message || '打开失败', code: 'OPEN_FAILED', matches }
    } catch (err) {
      console.error('文件搜索打开异常:', err)
      broadcastStatus('search_error', { error: err.message })
      return { success: false, error: '搜索异常，请稍后重试', code: 'SEARCH_ERROR' }
    }
  })

  // 专门：按应用名搜索并打开
  ipcMain.handle('search_open:app', async (event, keyword) => {
    const keyRaw = (typeof keyword === 'string' ? keyword.trim() : '')
    const key = keyRaw.toLowerCase()
    if (!key) {
      return { success: false, message: '关键字不能为空' }
    }
    broadcastStatus('searching', { keyword: key, scope: 'app' })

    // 仅检索应用候选
    const appCandidates = searchService ? (searchService.searchApps(key) || []) : []

    const getName = (item) => {
      if (!item) return ''
      if (item.name) return String(item.name).toLowerCase()
      if (item.path) {
        try { return path.basename(item.path).toLowerCase() } catch (_) { return '' }
      }
      return ''
    }

    // 1) 完全匹配优先
    let best = appCandidates.find(it => getName(it) === key)

    // 2) 若无完全匹配，进行递减前缀匹配
    if (!best) {
      for (let len = key.length - 1; len >= 1; len--) {
        const prefix = key.slice(0, len)
        best = appCandidates.find(it => getName(it).startsWith(prefix))
        if (best) break
      }
    }

    // 3) 若仍无匹配，则失败（不打开任何目标）
    if (!best) {
      broadcastStatus('search_empty', { keyword: key, scope: 'app' })
      broadcastStatus('open_failed', { message: `打开失败：未找到匹配的应用“${keyRaw}”` })
      return { success: false, message: '未找到匹配的应用' }
    }

    const label = getName(best)
    broadcastStatus('opening', { path: best.path, type: 'app', message: label ? `正在打开目标${label}` : '正在打开目标' })
    const res = await openService.open(best.path, 'app')
    const msg = res.success
      ? (label ? `已打开目标${label}` : '已打开目标')
      : (label ? `打开失败：${label}，原因：${res.message}` : `打开失败，原因：${res.message}`)
    broadcastStatus(res.success ? 'open_success' : 'open_failed', { message: msg, path: best.path, type: 'app' })
    return res
  })

  // 选择目录：仅允许目录，多选
  ipcMain.handle('dialog:select-dirs', async () => {
    const res = await dialog.showOpenDialog({ properties: ['openDirectory', 'multiSelections'] })
    const dirs = (res && res.filePaths) ? res.filePaths.filter(isDirectorySafe) : []
    return dirs
  })

  // 设置：获取工作目录数组（无则回退桌面路径）
  ipcMain.handle('settings:get-workdirs', () => {
    const dirs = readWorkDirs()
    if (dirs && dirs.length) return dirs
    const desktop = getDesktopDir()
    return isDirectorySafe(desktop) ? [desktop] : []
  })

  // 设置：保存工作目录数组（必须为目录），并广播更新
  ipcMain.handle('settings:set-workdirs', (event, dirs) => {
    const valid = (Array.isArray(dirs) ? dirs : []).filter(isDirectorySafe)
    if (!valid.length) {
      return { success: false, message: '请选择有效的目录路径' }
    }
    const ok = writeWorkDirs(valid)
    if (!ok) return { success: false, message: '保存失败，请重试' }
    try {
      const payload = { workDirs: valid }
      if (mainWindow && !mainWindow.isDestroyed()) {
        mainWindow.webContents.send('settings:workdirs-updated', payload)
      }
      if (floatWindow && !floatWindow.isDestroyed()) {
        floatWindow.webContents.send('settings:workdirs-updated', payload)
      }
    } catch (err) {
      console.error('广播工作目录更新失败:', err)
    }
    return { success: true, workDirs: valid }
  })

  // ===== 文件/目录操作：IPC 事件 =====
  const fsp = fs.promises
  const hasIllegalChars = (p) => /[<>:"|?*]/.test(p)
  const isAbsolutePath = (p) => {
    try { return typeof p === 'string' && p.length && path.isAbsolute(p) } catch (_) { return false }
  }
  const ensureParentDir = async (filePath, recursive) => {
    try {
      const parent = path.dirname(filePath)
      await fsp.mkdir(parent, { recursive: !!recursive })
      return true
    } catch (_) { return false }
  }

  // 创建文件：支持 options.overwrite 与 options.recursive（相对工作目录）
  ipcMain.handle('createFile', async (event, relPath, content = '', options = {}) => {
    try {
      const filePath = resolveWithinWorkRoot(relPath)
      if (!filePath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      const exists = fs.existsSync(filePath)
      if (exists && !options.overwrite) {
        return { success: false, error: '文件已存在', code: 'FILE_EXISTS' }
      }
      await ensureParentDir(filePath, options.recursive)
      const enc = typeof content === 'string' ? (options.encoding || 'utf8') : undefined
      await fsp.writeFile(filePath, content || '', enc ? { encoding: enc, flag: 'w' } : { flag: 'w' })
      return { success: true, path: filePath }
    } catch (err) {
      return { success: false, error: err.message || '创建文件失败' }
    }
  })

  // 写入文件：覆盖或追加（相对工作目录）
  ipcMain.handle('writeFile', async (event, relPath, content = '', options = {}) => {
    try {
      const filePath = resolveWithinWorkRoot(relPath)
      if (!filePath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      await ensureParentDir(filePath, options.recursive)
      const flag = options.flag === 'a' ? 'a' : 'w'
      const enc = typeof content === 'string' ? (options.encoding || 'utf8') : undefined
      if (flag === 'a') {
        await fsp.appendFile(filePath, content || '', enc ? { encoding: enc } : undefined)
      } else {
        await fsp.writeFile(filePath, content || '', enc ? { encoding: enc, flag } : { flag })
      }
      return { success: true, path: filePath }
    } catch (err) {
      return { success: false, error: err.message || '写入失败' }
    }
  })

  // 读取文件：支持编码（相对工作目录）
  ipcMain.handle('readFile', async (event, relPath, options = {}) => {
    try {
      const filePath = resolveWithinWorkRoot(relPath)
      if (!filePath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      if (!fs.existsSync(filePath)) return { success: false, error: '文件不存在', code: 'FILE_NOT_FOUND' }
      const enc = options.encoding || 'utf8'
      const data = await fsp.readFile(filePath, enc ? { encoding: enc } : undefined)
      return { success: true, content: data }
    } catch (err) {
      return { success: false, error: err.message || '读取失败' }
    }
  })

  // 删除文件：明确不存在返回失败（相对工作目录）
  ipcMain.handle('deleteFile', async (event, relPath) => {
    try {
      const filePath = resolveWithinWorkRoot(relPath)
      if (!filePath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      if (!fs.existsSync(filePath)) return { success: false, error: '文件不存在', code: 'FILE_NOT_FOUND' }
      await fsp.unlink(filePath)
      return { success: true, path: filePath }
    } catch (err) {
      return { success: false, error: err.message || '删除失败' }
    }
  })

  // 重命名文件：处理目标存在冲突（相对工作目录）
  ipcMain.handle('renameFile', async (event, fromRel, toRel, options = {}) => {
    try {
      const fromPath = resolveWithinWorkRoot(fromRel)
      const toPath = resolveWithinWorkRoot(toRel)
      if (!fromPath || !toPath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      if (!fs.existsSync(fromPath)) return { success: false, error: '源文件不存在', code: 'SRC_NOT_FOUND' }
      const targetExists = fs.existsSync(toPath)
      if (targetExists && !options.overwrite) {
        return { success: false, error: '目标已存在', code: 'TARGET_EXISTS' }
      }
      if (targetExists && options.overwrite) {
        try {
          const st = fs.statSync(toPath)
          if (st.isDirectory()) {
            if (fs.rm) await fs.promises.rm(toPath, { recursive: true, force: true })
            else await fs.promises.rmdir(toPath, { recursive: true })
          } else {
            await fsp.unlink(toPath)
          }
        } catch (_) { }
      }
      await ensureParentDir(toPath, true)
      await fsp.rename(fromPath, toPath)
      return { success: true, from: fromPath, to: toPath }
    } catch (err) {
      return { success: false, error: err.message || '重命名失败' }
    }
  })

  // 创建目录：支持 recursive（相对工作目录）
  ipcMain.handle('createDir', async (event, relDir, options = {}) => {
    try {
      const dirPath = resolveWithinWorkRoot(relDir)
      if (!dirPath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      await fsp.mkdir(dirPath, { recursive: !!options.recursive })
      return { success: true, path: dirPath }
    } catch (err) {
      return { success: false, error: err.message || '创建目录失败' }
    }
  })

  // 删除目录：默认仅删除空目录；options.force 递归强制删除（相对工作目录；禁止删除根目录）
  ipcMain.handle('deleteDir', async (event, relDir, options = {}) => {
    try {
      const dirPath = resolveWithinWorkRoot(relDir)
      if (!dirPath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      const root = getActiveWorkDir()
      if (path.resolve(dirPath) === path.resolve(root)) {
        return { success: false, error: '工作目录不可删除', code: 'ROOT_FORBIDDEN' }
      }
      if (options.force) {
        if (fs.rm) await fs.promises.rm(dirPath, { recursive: true, force: true })
        else await fs.promises.rmdir(dirPath, { recursive: true })
      } else {
        await fs.promises.rmdir(dirPath)
      }
      return { success: true, path: dirPath }
    } catch (err) {
      return { success: false, error: err.message || '删除目录失败' }
    }
  })

  // ===== 模板管理（项目根目录/template） =====
  const TEMPLATE_ROOT = path.join(process.cwd(), 'template')
  const ensureTemplateRoot = async () => {
    try { await fsp.mkdir(TEMPLATE_ROOT, { recursive: true }); return true } catch (_) { return false }
  }
  const sanitizeName = (s) => {
    const t = (typeof s === 'string' ? s.trim() : '')
    if (!t || hasIllegalChars(t)) return null
    return t
  }

  ipcMain.handle('template:list-categories', async () => {
    try {
      await ensureTemplateRoot()
      const entries = await fsp.readdir(TEMPLATE_ROOT, { withFileTypes: true })
      const cats = entries.filter(e => e.isDirectory()).map(e => e.name)
      return { success: true, categories: cats }
    } catch (err) {
      return { success: false, error: err.message || '读取分类失败' }
    }
  })

  ipcMain.handle('template:list-templates', async (event, category) => {
    try {
      const cat = sanitizeName(category)
      if (!cat) return { success: false, error: '分类名称非法' }
      const dir = path.join(TEMPLATE_ROOT, cat)
      if (!fs.existsSync(dir)) return { success: true, templates: [] }
      const entries = await fsp.readdir(dir, { withFileTypes: true })
      const files = entries.filter(e => e.isFile()).map(e => e.name)
      return { success: true, templates: files }
    } catch (err) {
      return { success: false, error: err.message || '读取模板失败' }
    }
  })

  ipcMain.handle('template:save', async (event, payload = {}) => {
    try {
      const cat = sanitizeName(payload.category)
      const nameRaw = sanitizeName(payload.name)
      const mime = typeof payload.mime === 'string' ? payload.mime : ''
      if (!cat || !nameRaw) return { success: false, error: '名称非法' }
      await ensureTemplateRoot()
      const dir = path.join(TEMPLATE_ROOT, cat)
      await fsp.mkdir(dir, { recursive: true })
      const ext = mime.includes('jpeg') ? '.jpg' : (mime.includes('png') ? '.png' : (mime.includes('bmp') ? '.bmp' : '.png'))
      const fileName = nameRaw.endsWith(ext) ? nameRaw : (nameRaw + ext)
      const filePath = path.join(dir, fileName)
      let buf
      const data = payload.data
      if (typeof data === 'string' && data.startsWith('data:')) {
        const base64 = data.split(',')[1] || ''
        buf = Buffer.from(base64, 'base64')
      } else if (data && (data.type === 'Buffer' || data instanceof Uint8Array)) {
        buf = Buffer.from(data)
      } else {
        return { success: false, error: '无效的图片数据' }
      }
      await ensureParentDir(filePath, true)
      await fsp.writeFile(filePath, buf)
      return { success: true, path: filePath, fileName }
    } catch (err) {
      return { success: false, error: err.message || '保存模板失败' }
    }
  })

  ipcMain.handle('template:delete', async (event, payload = {}) => {
    try {
      const cat = sanitizeName(payload.category)
      const name = sanitizeName(payload.name)
      if (!cat || !name) return { success: false, error: '名称非法' }
      const filePath = path.join(TEMPLATE_ROOT, cat, name)
      if (!fs.existsSync(filePath)) return { success: false, error: '模板不存在' }
      await fsp.unlink(filePath)
      return { success: true }
    } catch (err) {
      return { success: false, error: err.message || '删除模板失败' }
    }
  })

  ipcMain.handle('template:delete-category', async (event, category) => {
    try {
      const cat = sanitizeName(category)
      if (!cat) return { success: false, error: '分类名称非法' }
      const dir = path.join(TEMPLATE_ROOT, cat)
      if (!fs.existsSync(dir)) return { success: false, error: '分类不存在' }
      if (fs.rm) await fs.promises.rm(dir, { recursive: true, force: true })
      else await fs.promises.rmdir(dir, { recursive: true })
      return { success: true }
    } catch (err) {
      return { success: false, error: err.message || '删除分类失败' }
    }
  })

  ipcMain.handle('template:get-path', async (event, payload = {}) => {
    try {
      const cat = sanitizeName(payload.category)
      const name = sanitizeName(payload.name)
      if (!cat || !name) return { success: false, error: '名称非法' }
      const filePath = path.join(TEMPLATE_ROOT, cat, name)
      if (!fs.existsSync(filePath)) return { success: false, error: '模板不存在' }
      return { success: true, path: filePath }
    } catch (err) {
      return { success: false, error: err.message || '获取路径失败' }
    }
  })

  ipcMain.handle('template:get-dataurl', async (event, payload = {}) => {
    try {
      const cat = sanitizeName(payload.category)
      const name = sanitizeName(payload.name)
      if (!cat || !name) return { success: false, error: '名称非法' }
      const filePath = path.join(TEMPLATE_ROOT, cat, name)
      if (!fs.existsSync(filePath)) return { success: false, error: '模板不存在' }
      const buf = await fsp.readFile(filePath)
      const ext = path.extname(filePath).toLowerCase()
      const mime = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : (ext === '.png' ? 'image/png' : 'image/bmp')
      const dataURL = `data:${mime};base64,${buf.toString('base64')}`
      return { success: true, dataURL }
    } catch (err) {
      return { success: false, error: err.message || '读取图片失败' }
    }
  })
  // 读取目录内容：返回名称、类型、修改时间、大小（相对工作目录）
  ipcMain.handle('readDir', async (event, relDir) => {
    try {
      const dirPath = resolveWithinWorkRoot(relDir)
      if (!dirPath) {
        return { success: false, error: '非法路径：需在工作目录内且不含特殊字符' }
      }
      const st = fs.existsSync(dirPath) ? fs.statSync(dirPath) : null
      if (!st || !st.isDirectory()) return { success: false, error: '目录不存在', code: 'DIR_NOT_FOUND' }
      const entries = await fsp.readdir(dirPath, { withFileTypes: true })
      const list = []
      for (const e of entries) {
        const full = path.join(dirPath, e.name)
        let stat
        try { stat = await fsp.stat(full) } catch (_) { continue }
        list.push({ name: e.name, path: full, type: e.isDirectory() ? 'dir' : 'file', mtime: stat.mtimeMs, size: stat.size })
      }
      return { success: true, items: list }
    } catch (err) {
      return { success: false, error: err.message || '读取目录失败' }
    }
  })

  // 搜索：返回排序后的结果集
  ipcMain.handle('search:target', async (event, keyword) => {
    const key = (typeof keyword === 'string' ? keyword.trim() : '')
    if (!key) {
      return { success: false, message: '关键字不能为空', results: [] }
    }
    broadcastStatus('searching', { keyword: key })
    try {
      const results = searchService ? searchService.search(key) : []
      if (!results.length) {
        broadcastStatus('search_empty', { keyword: key })
        return { success: false, message: '未找到匹配的应用或文件', results: [] }
      }
      broadcastStatus('search_done', { count: results.length })
      return { success: true, results }
    } catch (err) {
      console.error('搜索失败:', err)
      broadcastStatus('search_error', { error: err.message })
      return { success: false, message: '搜索异常，请稍后重试', results: [] }
    }
  })

  // 截屏
  ipcMain.handle('take-screenshot', async (event) => {
    try {
      // 动态匹配屏幕分辨率（考虑缩放因子），强制与屏幕大小一致
      const display = screen.getPrimaryDisplay()
      const scale = (display && typeof display.scaleFactor === 'number') ? display.scaleFactor : 1
      const targetWidth = Math.max(1, Math.round(((display && display.size && display.size.width) ? display.size.width : 1920) * scale))
      const targetHeight = Math.max(1, Math.round(((display && display.size && display.size.height) ? display.size.height : 1080) * scale))

      const sources = await desktopCapturer.getSources({
        types: ['screen'],
        thumbnailSize: { width: targetWidth, height: targetHeight }
      });

      if (sources && sources.length > 0) {
        // 获取第一个屏幕的截图（主屏幕）
        const screenshot = sources[0].thumbnail;

        // 转换为base64
        const screenshotDataURL = screenshot.toDataURL();

        // 或者保存为文件
        const buffer = screenshot.toPNG();
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const screenshotsDir = path.join(process.cwd(), "screenshots");
        if (!fs.existsSync(screenshotsDir)) {
          fs.mkdirSync(screenshotsDir, { recursive: true });
        }

        const filePath = path.join(screenshotsDir, `screenshot-${timestamp}.png`)

        fs.writeFileSync(filePath, buffer);

        return {
          success: true,
          dataURL: screenshotDataURL,
          filePath: filePath,
          message: '截屏成功'
        };
      } else {
        return {
          success: false,
          error: '未找到可用的屏幕'
        };
      }
    } catch (error) {
      console.error('截屏失败:', error);
      return {
        success: false,
        error: error.message
      };
    }

  })

  // ===== 系统模拟操作：鼠标、键盘、滚轮 =====
  ipcMain.handle('simulate:mouse', async (event, payload = {}) => {
    try {
      const { x, y, width, height, button } = payload
      if (typeof x !== 'number' || typeof y !== 'number') {
        return { success: false, error: '坐标参数无效' }
      }
      const tx = typeof width === 'number' ? Math.round(x + width / 2) : Math.round(x)
      const ty = typeof height === 'number' ? Math.round(y + height / 2) : Math.round(y)

      if (robot) {
        robot.moveMouse(tx, ty)
        if (button === 'right') {
          robot.mouseClick('right')
        } else if (button === 'doubleClick') {
          // 左键双击
          robot.mouseClick('left', true)
        } else {
          robot.mouseClick('left')
        }
        return { success: true }
      }
      if (nut) {
        const { mouse, Point } = nut
        await mouse.setPosition(new Point(tx, ty))
        if (button === 'right') {
          await mouse.rightClick()
        } else if (button === 'doubleClick') {
          // 左键双击（nut-js 兼容回退：两次左键点击）
          await mouse.leftClick()
          await mouse.leftClick()
        } else {
          await mouse.leftClick()
        }
        return { success: true }
      }
      return { success: false, error: 'robotjs 未安装或加载失败，且未找到 nut-js 回退' }
    } catch (err) {
      return { success: false, error: err.message || '鼠标操作失败' }
    }
  })

  ipcMain.handle('simulate:keyboard', async (event, payload = {}) => {
    try {
      const { text = '', specialKeys = [], combos = [], order, forcePaste = false, delayBeforeTextMs = 150, waitBetweenStepsMs = 50 } = payload

      // 小延时辅助 IME/目标应用处理
      const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

      // 解析执行顺序：允许 ['combos','text','specialKeys']，默认 combos -> text -> specialKeys
      const normalize = (s) => String(s).toLowerCase()
      const validSteps = new Set(['combos', 'text', 'specialkeys'])
      const defaultOrder = ['combos', 'text', 'specialkeys']
      const seq = Array.isArray(order)
        ? order.map(normalize).filter((s) => validSteps.has(s))
        : defaultOrder
      const steps = seq.length ? seq : defaultOrder

      const sText = String(text)
      const hasNonAscii = /[^\x00-\x7F]/.test(sText)
      const shouldPaste = forcePaste || hasNonAscii

      // 封装：执行单步（robotjs）
      const runRobotStep = async (step) => {
        if (step === 'combos') {
          for (const c of combos) {
            const cs = Array.isArray(c) ? c.map(normalize) : String(c).toLowerCase().split('+').map(normalize)
            if ((cs[0] === 'ctrl' || cs[0] === 'control') && cs[1] === 'a') {
              try { robot.keyTap('a', 'control') } catch (_) { }
            }
          }
          // 兼容平铺写法：['ctrl','a']
          try {
            const flat = Array.isArray(combos) && combos.length && combos.every((x) => typeof x === 'string' && !String(x).includes('+'))
            if (flat) {
              const l = combos.map(normalize)
              if (l.some((k) => k === 'ctrl' || k === 'control') && l.includes('a')) {
                try { robot.keyTap('a', 'control') } catch (_) { }
              }
            }
          } catch (_) {}
          await wait(waitBetweenStepsMs)
        } else if (step === 'text') {
          if (sText) {
            await wait(delayBeforeTextMs)
            try {
              if (shouldPaste) {
                try { clipboard.writeText(sText) } catch (_) {}
                await wait(50)
                try { robot.keyToggle('control', 'down') } catch (_) {}
                await wait(10)
                try { robot.keyTap('v') } catch (_) {}
                await wait(10)
                try { robot.keyToggle('control', 'up') } catch (_) {}
              } else {
                robot.typeString(sText)
              }
            } catch (_) {
              // 回退：统一剪贴板粘贴
              try {
                clipboard.writeText(sText)
                await wait(50)
                robot.keyToggle('control', 'down')
                await wait(10)
                robot.keyTap('v')
                await wait(10)
                robot.keyToggle('control', 'up')
              } catch (_) {}
            }
          }
          await wait(waitBetweenStepsMs)
        } else if (step === 'specialkeys') {
          // 兼容平铺写法：['ctrl','a'] 放在 specialKeys 中
          try {
            const lower = specialKeys.map((x) => String(x).toLowerCase())
            const hasPlus = lower.some((x) => x.includes('+'))
            if (!hasPlus && lower.some((x) => x === 'ctrl' || x === 'control') && lower.includes('a')) {
              try { robot.keyTap('a', 'control') } catch (_) { }
            }
          } catch (_) {}
          for (const k of specialKeys) {
            const ks = String(k)
            if (ks.includes('+')) {
              const parts = ks.toLowerCase().split('+').map(normalize)
              if ((parts[0] === 'ctrl' || parts[0] === 'control') && parts[1] === 'a') {
                try { robot.keyTap('a', 'control') } catch (_) { }
              }
            } else {
              const name = ks.toLowerCase()
              if (name === 'ctrl' || name === 'control' || name === 'a') continue
              try { robot.keyTap(name) } catch (_) { }
            }
          }
          await wait(waitBetweenStepsMs)
        }
      }

      // 封装：执行单步（nut-js）
      const runNutStep = async (step) => {
        const { keyboard, Key } = nut
        if (step === 'combos') {
          for (const c of combos) {
            const cs = Array.isArray(c) ? c.map(normalize) : String(c).toLowerCase().split('+').map(normalize)
            if ((cs[0] === 'ctrl' || cs[0] === 'control') && cs[1] === 'a') {
              try { await keyboard.pressKey(Key.LeftControl, Key.A); await keyboard.releaseKey(Key.LeftControl, Key.A) } catch (_) { }
            }
          }
          // 兼容平铺写法：['ctrl','a']
          try {
            const flat = Array.isArray(combos) && combos.length && combos.every((x) => typeof x === 'string' && !String(x).includes('+'))
            if (flat) {
              const l = combos.map(normalize)
              if (l.some((k) => k === 'ctrl' || k === 'control') && l.includes('a')) {
                try { await keyboard.pressKey(Key.LeftControl, Key.A); await keyboard.releaseKey(Key.LeftControl, Key.A) } catch (_) { }
              }
            }
          } catch (_) {}
          await wait(waitBetweenStepsMs)
        } else if (step === 'text') {
          if (sText) {
            await wait(delayBeforeTextMs)
            try {
              if (shouldPaste) {
                try { clipboard.writeText(sText) } catch (_) {}
                await wait(50)
                try { await keyboard.pressKey(Key.LeftControl) } catch (_) {}
                await wait(10)
                try { await keyboard.type('v') } catch (_) {}
                await wait(10)
                try { await keyboard.releaseKey(Key.LeftControl) } catch (_) {}
              } else {
                await keyboard.type(sText)
              }
            } catch (_) {
              try {
                clipboard.writeText(sText)
                await wait(50)
                await keyboard.pressKey(Key.LeftControl)
                await wait(10)
                await keyboard.type('v')
                await wait(10)
                await keyboard.releaseKey(Key.LeftControl)
              } catch (_) {}
            }
          }
          await wait(waitBetweenStepsMs)
        } else if (step === 'specialkeys') {
          const map = {
            enter: Key.Enter,
            tab: Key.Tab,
            escape: Key.Escape,
            space: Key.Space,
            backspace: Key.Backspace,
            delete: Key.Delete,
            up: Key.Up,
            down: Key.Down,
            left: Key.Left,
            right: Key.Right,
          }
          for (const k of specialKeys) {
            const ks = String(k)
            if (ks.includes('+')) continue // 组合键已在 combos 步处理
            const kk = ks.toLowerCase()
            if (map[kk]) {
              try { await keyboard.type(map[kk]) } catch (_) { }
            }
          }
          await wait(waitBetweenStepsMs)
        }
      }

      if (robot) {
        for (const step of steps) {
          await runRobotStep(step)
        }
        return { success: true }
      }
      if (nut) {
        for (const step of steps) {
          await runNutStep(step)
        }
        return { success: true }
      }
      return { success: false, error: 'robotjs 未安装或加载失败，且未找到 nut-js 回退' }
    } catch (err) {
      return { success: false, error: err.message || '键盘操作失败' }
    }
  })

  ipcMain.handle('simulate:scroll', async (event, payload = {}) => {
    try {
      const { direction, amount } = payload
      if (!['vertical', 'horizontal'].includes(direction) || typeof amount !== 'number') {
        return { success: false, error: '滚轮参数无效' }
      }
      if (robot) {
        if (direction === 'vertical') robot.scrollMouse(0, Math.round(amount))
        else robot.scrollMouse(Math.round(amount), 0)
        return { success: true }
      }
      if (nut) {
        const { mouse } = nut
        const steps = Math.abs(Math.round(amount)) || 0
        if (steps === 0) return { success: true }
        if (direction === 'vertical') {
          if (amount > 0) await mouse.scrollDown(steps)
          else await mouse.scrollUp(steps)
        } else {
          if (amount > 0) await mouse.scrollRight(steps)
          else await mouse.scrollLeft(steps)
        }
        return { success: true }
      }
      return { success: false, error: 'robotjs 未安装或加载失败，且未找到 nut-js 回退' }
    } catch (err) {
      return { success: false, error: err.message || '滚轮操作失败' }
    }
  })

  // ... existing code ...
}

function init() {
  createMainWindow()
  createFloatWindow()
  createTray()
  registerIpc()
}

function initSearchEngine() {
  try {
    appIndexer = new AppIndexer()
    registryIndexer = new RegistryIndexer()
    fileIndexer = new FileIndexer()
    fileSearchEngine = new FileSearchEngine()
    openService = new OpenService()
    // 构建索引（开始菜单 + 注册表）
    const startApps = appIndexer.buildIndex()
    const regApps = registryIndexer.buildIndex()
    fileIndexer.buildIndex()

    // 合并去重应用索引
    const map = new Map()
    for (const it of [...startApps, ...regApps]) {
      const key = `${(it.name || '').toLowerCase()}|${it.path.toLowerCase()}`
      if (!map.has(key)) map.set(key, it)
    }
    const mergedApps = Array.from(map.values())
    const combinedAppIndexer = { getAll: () => mergedApps }

    searchService = new SearchService(combinedAppIndexer, fileIndexer)
    const fileCount = (fileIndexer.getAll() || []).length
    broadcastStatus('index_ready', { appCount: mergedApps.length, fileCount })
  } catch (err) {
    console.error('索引构建失败:', err)
    broadcastStatus('index_failed', { error: err.message })
  }
}

app.whenReady().then(() => {
  app.setName('声声不息');
  if (process.platform === 'win32') {
    app.setAppUserModelId('com.huxiaohui.ssbx');
  }
  Menu.setApplicationMenu(null)
  app.on('browser-window-created', (_, win) => win.setMenuBarVisibility(false))
  // registerOpenCvHandlers()
  init()
  initSearchEngine()
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) init()
  })
})

// 阻止所有窗口关闭后退出应用，保持托盘常驻
app.on('window-all-closed', (e) => {
  if (!forceQuit) {
    e.preventDefault()
  }
})